
import java.io.IOException;
import java.util.List;
import java.util.Random;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.scene.Spatial;


public class Chunk implements Savable{
	
	public BlockVisibility[][][] visibility;
	
	float[][][] chunk;
	Octree octree;
	static int octaveCount = 6;
	int offsetX;
	int offsetZ;
	ChunkType type;
	int id;

	static int width = 16;
	static int height = 32;
	static int depth = 16;
	
	enum ChunkType{
		SOLID, PERLIN3D, EVERYOTHER;
	}
	
	//3d Perlin noise adapted from this 2d perlin noise algorithm http://devmag.org.za/2009/04/25/perlin-noise/
	public Chunk(int id, int x, int z)
	{
		this.id = id;
		offsetX = x;
		offsetZ = z;
		Engine.getEngine().getWorld().nextChunkID++;
		type = ChunkType.SOLID;
		
		if(type == ChunkType.SOLID)
			generateSolid();
		else if(type == ChunkType.PERLIN3D)
			generatePerlin3d();
		else if(type == ChunkType.EVERYOTHER)
			generateEveryOther();
		//octree = generateOctree();
		//singleOutTopLevelChild(octree, 0);
	}
	
	//generate an octree using chunk data within the range specified
	//max depth of 5
	public Octree generateOctree()
	{
		Octree octree = generateOctChildren(null, 0, Chunk.width, 0, Chunk.height, 0, Chunk.depth);
		octree.offsetX = offsetX;
		octree.offsetY = 0;
		octree.offsetZ = offsetZ;
		octree.generateOffsets();
		
		return octree;
	}
	
	public void singleOutTopLevelChild(Octree oct, int childNum)
	{
		for(int i = 0; i < oct.child.length; i++)
		{
			if(oct.child[i] != null && i == childNum)
			{
				setChildrenType(oct.child[i], 2);
			}
		}
	}
	
	public void setChildrenType(Octree oct, int type)
	{
		if(oct.type != -1)
			oct.type = (byte) type;
		for(int i = 0; i < oct.child.length; i++)
		{
			if(oct.child[i] != null)
				setChildrenType(oct.child[i], type);
		}
	}
	
	public void singleOutChildNum(Octree oct, int childNum)
	{
		for(int i = 0; i < oct.child.length; i++)
		{
			if(oct.child[i] != null)
			{
				singleOutChildNum(oct.child[i], childNum);
				if(i == childNum)
					oct.child[i].type = 2;
			}
		}
	}
	
	public Octree generateOctreeFilledToDepth(int depth)
	{
		Octree octree = generateOct(null, depth);
		octree.offsetX = offsetX;
		octree.offsetY = 0;
		octree.offsetZ = offsetZ;
		octree.generateOffsets();
		
		return octree;
	}
	
	public Octree generateOct(Octree parent, int depth)
	{
		Octree o = new Octree(parent);
		if(o.getDepth(o) < depth)
		{
			o.type = -1;
			o.child[0] = generateOct(o, depth);
			o.child[1] = generateOct(o, depth);
			o.child[2] = generateOct(o, depth);
			o.child[3] = generateOct(o, depth);
			o.child[4] = generateOct(o, depth);
			o.child[5] = generateOct(o, depth);
			o.child[6] = generateOct(o, depth);
			o.child[7] = generateOct(o, depth);
		}
		else
			o.type = 1;
		return o;
	}
	
	public Octree generateOctChildren(Octree parent, int x, int x2, int y, int y2, int z, int z2)
	{
		Octree oct = new Octree(parent);
		boolean contiguous = true;
		byte type = (byte) chunk[x][y][z];
		for(int i = x; i < x2; i++)
		{
			for(int j = y; j < y2; j++)
			{
				for(int k = z; k < z2; k++)//z++ because we take first z as type to check for contiguity
				{
					if(chunk[i][j][k] != type && contiguous == true)
					{
						//System.out.println("x: " + x + "-" + x2 + " y: " + y + "-" + y2 + " z: " + z + "-" + z2);
						contiguous = false;
						
						oct.child[0] = generateOctChildren(oct, x, x2/2 + x/2, y, y2/2 + y/2, z, z2/2 + z/2);
						oct.child[1] = generateOctChildren(oct, x2/2 + x/2, x2, y, y2/2 + y/2, z, z2/2 + z/2);
						oct.child[2] = generateOctChildren(oct, x2/2 + x/2, x2, y, y2/2 + y/2, z2/2 + z/2, z2);
						oct.child[3] = generateOctChildren(oct, x, x2/2 + x/2, y, y2/2 + y/2, z2/2 + z/2, z2);
						oct.child[4] = generateOctChildren(oct, x, x2/2 + x/2, y2/2 + y/2, y2, z, z2/2 + z/2);
						oct.child[5] = generateOctChildren(oct, x2/2 + x/2, x2, y2/2 + y/2, y2, z, z2/2 + z/2);
						oct.child[6] = generateOctChildren(oct, x2/2 + x/2, x2, y2/2 + y/2, y2, z2/2 + z/2, z2);
						oct.child[7] = generateOctChildren(oct, x, x2/2 + x/2, y2/2 + y/2, y2, z2/2 + z/2, z2);
					}
				}
			}
		}
		if(contiguous == true)
		{
			oct.type = (byte) type;
			oct.chunkId = id;

		}
		else//type defaults to -1, which is no material.  0 is air.  This signifies that the children hold type info
			oct.type = -1;
		
		System.out.println("Depth: " + oct.getDepth(oct) + " Type: " + oct.type);
		return oct;
	}
	
	public void generateSolid()
	{
		chunk = new float[Chunk.width][Chunk.height][Chunk.depth];
		for(int i = 0; i < chunk.length; i++)
		{
			for(int j = 0; j < chunk[0].length; j++)
			{
				for(int k = 0; k < chunk[0][0].length; k++)
					chunk[i][j][k] = 1;
			}
		}
	}
	
	public void generateEveryOther()
	{
		chunk = new float[Chunk.width][Chunk.height][Chunk.depth];
		int counter = 0, counter2 = 0, counter3 = 0;
		for(int i = 0; i < chunk.length; i++)
		{
			for(int j = 0; j < chunk[0].length; j++)
			{
				for(int k = 0; k < chunk[0][0].length; k++)
				{
					if(counter%2==0 && counter2%2 == 0 && counter3%2 == 0)
						chunk[i][j][k] = 1;
					else
						chunk[i][j][k] = 0;
					counter++;
				}
				counter2++;
			}
			counter3++;
		}
	}
	
	public void generatePerlin3d()
	{
		Random r = new Random();
		float[][][] noise = new float[Chunk.width][Chunk.height][Chunk.depth];
		
		for(int i = 0; i < noise.length; i++)
		{
			for(int j = 0; j < noise[0].length; j++)
			{
				for(int k = 0; k < noise[0][0].length; k++)
				{
					noise[i][j][k] = r.nextFloat() % 1;
				}
			}
		}
		
		float[][][][] octaves = new float[octaveCount][][][];
		
		for(int i = 0; i < octaveCount; i++)
		{
			octaves[i] = generateOctave(noise, i);
		}
		
		chunk = new float[Chunk.width][Chunk.height][Chunk.depth];
		float amplitude = 1;
		float totalAmplitude = 0;
		float persistence = .7f;
		
		for(int octave = octaveCount - 1; octave >= 0; octave--)
		{
			amplitude *= persistence;
			totalAmplitude += amplitude;
			
			for(int i = 0; i < Chunk.width; i++)
			{
				for(int j = 0; j < Chunk.height; j++)
				{
					for(int k = 0; k < Chunk.depth; k++)
					{
						chunk[i][j][k] += octaves[octave][i][j][k] * amplitude;
					}
				}
			}
		}
		
		//normalization
		for(int i = 0; i < Chunk.width; i++)
		{
			for(int j = 0; j < Chunk.height; j++)
			{
				for(int k = 0; k < Chunk.depth; k++)
				{
					chunk[i][j][k] /= totalAmplitude;
					
					//assign block types - currently set so all blocks are solid
					if(chunk[i][j][k] > .5)
						chunk[i][j][k] = 0;
					else
						chunk[i][j][k] = 1;
					//System.out.print(chunk[i][j][k] + " ");
				}
				//System.out.println();
			}
			//System.out.println("Next Slice");
		}
		
		/*
		//zero out edges for debugging
		for(int i = 0; i < World.width; i++)
		{
			for(int j = 0; j < World.height; j++)
			{
				for(int k = 0; k < World.depth; k++)
				{
					chunk[0][j][k] = 0;
					chunk[World.width - 1][j][k] = 0;
					chunk[i][0][k] = 0;
					chunk[i][World.height - 1][k] = 0;
					chunk[i][j][0] = 0;
					chunk[i][j][World.depth - 1] = 0;
				}
			}
		}
		*/
	}
	
	public float[][][] generateOctave(float[][][] baseNoise, int octave)
	{	
		float[][][] smoothNoise = new float[baseNoise.length][baseNoise[0].length][baseNoise[0][0].length];
		
		int samplePeriod = 1 << octave;
		float sampleFrequency = 1 / samplePeriod;
		
		for(int i = 0; i < Chunk.width; i++)
		{
			int sample_i0 = (i / samplePeriod) * samplePeriod;
			int sample_i1 = (sample_i0 + samplePeriod) % Chunk.width;
			float horizontal_blend = (i - sample_i0) * sampleFrequency;
			
			for(int j = 0; j < Chunk.height; j++)
			{
				int sample_j0 = (j / samplePeriod) * samplePeriod;
				int sample_j1 = (sample_j0 + samplePeriod) % Chunk.height;
				float vertical_blend = (j - sample_j0) * sampleFrequency;
				
				for(int k = 0; k < Chunk.depth; k++)
				{
					int sample_k0 = (k / samplePeriod) * samplePeriod;
					int sample_k1 = (sample_k0 + samplePeriod) % Chunk.depth;
					float depth_blend = (k - sample_k0) * sampleFrequency;
					
					//Blend closest slice
					float top = interpolate(baseNoise[sample_i0][sample_j0][sample_k0], baseNoise[sample_i1][sample_j0][sample_k0], horizontal_blend);
					float bottom = interpolate(baseNoise[sample_i0][sample_j1][sample_k0], baseNoise[sample_i1][sample_j1][sample_k0], horizontal_blend);
					
					float closest = interpolate(top, bottom, vertical_blend);
					
					//Blend furthest slice
					top = interpolate(baseNoise[sample_i0][sample_j0][sample_k1], baseNoise[sample_i1][sample_j0][sample_k1], horizontal_blend);
					bottom = interpolate(baseNoise[sample_i0][sample_j1][sample_k1], baseNoise[sample_i1][sample_j1][sample_k1], horizontal_blend);
					
					float furthest = interpolate(top, bottom, vertical_blend);
					
					smoothNoise[i][j][k] = interpolate(closest, furthest, depth_blend);
				}
			}
		}
		return smoothNoise;
	}
	
	public float interpolate(float x, float y, float alpha)
	{
		return x * (1 - alpha) + y * alpha;
	}
	
	
	public void eraseBlock(int x, int y, int z)
	{
		chunk[x][y][z] = BlockType.AIR;
		
		Engine.getEngine().getWorld().getWorldNode().detachChildNamed(offsetX + "^" + offsetZ + ":" + x + "-" + y + "-" + z + "N");
		Engine.getEngine().getWorld().getWorldNode().detachChildNamed(offsetX + "^" + offsetZ + ":" + x + "-" + y + "-" + z + "S");
		Engine.getEngine().getWorld().getWorldNode().detachChildNamed(offsetX + "^" + offsetZ + ":" + x + "-" + y + "-" + z + "E");
		Engine.getEngine().getWorld().getWorldNode().detachChildNamed(offsetX + "^" + offsetZ + ":" + x + "-" + y + "-" + z + "W");
		Engine.getEngine().getWorld().getWorldNode().detachChildNamed(offsetX + "^" + offsetZ + ":" + x + "-" + y + "-" + z + "T");
		Engine.getEngine().getWorld().getWorldNode().detachChildNamed(offsetX + "^" + offsetZ + ":" + x + "-" + y + "-" + z + "B");
		
		Renderer.drawBlock(this, x - 1, y, z);
		Renderer.drawBlock(this, x + 1, y, z);
		Renderer.drawBlock(this, x, y - 1, z);
		Renderer.drawBlock(this, x, y + 1, z);
		Renderer.drawBlock(this, x, y, z - 1);
		Renderer.drawBlock(this, x, y, z + 1);
	}

	public void read(JmeImporter im) throws IOException {
		InputCapsule cap = im.getCapsule(this);
		
		
		float[][] defValue = {{-1}, {-1}};
		for(int i = 0; i < Chunk.width - 1; i++)
		{
			chunk[i] = cap.readFloatArray2D("blockSlice" + i, defValue);
		}
		offsetX = cap.readInt("offsetX", -1);
		offsetZ = cap.readInt("offsetZ", -1);
	}

	public void write(JmeExporter ex) throws IOException {
		OutputCapsule cap = ex.getCapsule(this);
		
		//Odd slicing technique is necessary because cap.write can only accept 2d arrays
		float[][] defValue = {{-1}, {-1}};
		for(int i = 0; i < Chunk.width - 1; i++)
		{
			float[][] cSlice = new float[Chunk.height][Chunk.depth];
			for(int j = 0; j < Chunk.height; j++)
			{
				for(int k = 0; k < Chunk.depth; k++)
				{
					cSlice[j][k] = chunk[i][j][k];
				}
			}
			cap.write(cSlice, "blockSlice" + i, defValue);
		}
		
		cap.write(offsetX, "offsetX", -1);
		cap.write(offsetZ, "offsetZ", -1);
	}
}
